/**
 * Copyright (c) 2009 Teoti Graphix, LLC See:
 *     http://tkarchitecture.teotigraphix.org
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 * 
 * @author Michael Schmalle mschmalle@teotigraphix.com
 */
package org.teotigraphix.dx.patterns.manager.model
{

import flash.display.DisplayObject;
import flash.events.Event;
import flash.events.IEventDispatcher;

import mx.events.PropertyChangeEvent;

import org.teotigraphix.dx.core.ToolKitUtil;
import org.teotigraphix.dx.core.dx_internal;
import org.teotigraphix.dx.event.ComponentLayerEvent;
import org.teotigraphix.dx.event.ModelLayerEvent;
import org.teotigraphix.dx.interfaces.IComponentModel;
import org.teotigraphix.dx.interfaces.IComponentModelCore;
import org.teotigraphix.dx.interfaces.ICoreModelAware;
import org.teotigraphix.dx.interfaces.IModelLayer;
import org.teotigraphix.dx.interfaces.IModelLayerAware;
import org.teotigraphix.dx.interfaces.IModelManager;
import org.teotigraphix.dx.interfaces.IToolKitComponent;
import org.teotigraphix.dx.patterns.manager.core.LayerRegistry;
import org.teotigraphix.dx.patterns.manager.meta.ClassMetaData;
import org.teotigraphix.dx.patterns.manager.meta.ModelBindingInfo;

[Mixin]

/**
 * @private
 * 
 * The default <code>IModelManager</code> implementation for the toolkit.
 * 
 * <p><em>This class is internal to the toolkit.</em></p>
 * 
 * @author Michael Schmalle
 * @copyright Teoti Graphix, LLC
 * @date 07-20-09
 */
public class ModelManagerImpl
	   implements IModelManager
{
	include '../../../core/Version.as'

	//--------------------------------------------------------------------------
	//
	//  Public Static Initialization
	//
	//--------------------------------------------------------------------------
	
	ToolKitUtil.registerSingleton(
		"org.teotigraphix.dx.interfaces::IModelManager", 
		ModelManagerImpl);
	
	//--------------------------------------------------------------------------
	//
	//  Public Mixin :: Methods - ISystemManager
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @private
	 */
	public static function init(root:DisplayObject):void
	{
	}
	
	//--------------------------------------------------------------------------
	//
	//  Private Class :: Properties
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @private
	 */
	private static var instance:IModelManager = null;
	
	//--------------------------------------------------------------------------
	//
	//  Constructor
	//
	//--------------------------------------------------------------------------
	
	/**
	 * Constructor.
	 * @private
	 */
	public function ModelManagerImpl()
	{
	}
	
	//--------------------------------------------------------------------------
	//
	//  Private Class :: Properties
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @private
	 */
	private static var blockChangeEvents:Boolean = false;
	
	/**
	 * @private
	 */
	private static var registry:LayerRegistry = new ModelRegistry();
	
	//--------------------------------------------------------------------------
	//
	//  IModelManager API :: Methods
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @copy org.teotigraphix.tka.patterns.manager.IModelManager#createModelLayer()
	 */
	public function createModelLayer(component:IEventDispatcher):IModelLayer
	{
		return registry.createLayer(component) as IModelLayer;
	}
	
	/**
	 * @copy org.teotigraphix.tka.patterns.manager.IModelManager#getModelLayer()
	 */
	public function getModelLayer(component:IEventDispatcher):IModelLayer
	{
		return registry.getLayer(component) as IModelLayer;
	}
		
	/**
	 * @copy org.teotigraphix.tka.patterns.manager.IModelManager#hasModelLayer()
	 */
	public function hasModelLayer(component:IEventDispatcher):Boolean
	{
		return registry.hasLayer(component);
	}
	
	// TODO creat unit test
		
	/**
	 * @copy org.teotigraphix.tka.patterns.manager.IModelManager#registerModelAPI()
	 */
	public function registerModelAPI(
		api:Class, clazz:Class):void
	{
		registry.registerAPI(api, clazz);
	}
	
	// TODO creat unit test
	
	/**
	 * @copy org.teotigraphix.tka.patterns.manager.IModelManager#createModel()
	 */
	public function createModel(
		component:IEventDispatcher, api:Class):IComponentModel
	{
		return registry.createClient(component, api) as IComponentModel;
	}
	
	/**
	 * @copy org.teotigraphix.tka.patterns.manager.IModelManager#registerModel()
	 */
	public function registerModel(
		component:IEventDispatcher, api:Class, behavior:IComponentModel):void
	{
		registry.registerClient(component, api, behavior);
	}
		
	/**
	 * @copy org.teotigraphix.tka.patterns.manager.IModelManager#removeModel()
	 */
	public function removeModel(
		component:IEventDispatcher, api:Class):IComponentModel
	{
		return registry.removeClient(component, api) as IComponentModel;
	}
		
	/**
	 * @copy org.teotigraphix.tka.patterns.manager.IModelManager#hasModel()
	 */
	public function hasModel(
		component:IEventDispatcher, api:Class):Boolean
	{
		return registry.hasClient(component, api);
	}
		
	/**
	 * @copy org.teotigraphix.tka.patterns.manager.IModelManager#hasModel()
	 */
	public function getModel(
		component:IEventDispatcher, api:Class):IComponentModel
	{
		return registry.getClient(component, api) as IComponentModel;
	}
		
	/**
	 * @copy org.teotigraphix.tka.patterns.manager.IModelManager#getModels()
	 */
	public function getModels(
		component:IEventDispatcher):Array
	{
		return registry.getClients(component);
	}	
	
	/**
	 * @private
	 */
	public function activate(host:IModelLayerAware, layer:IModelLayer):void
	{
		layer.addEventListener(
			ComponentLayerEvent.LAYER_ACTIVATE,
			modelLayerActivateHandler);
		layer.addEventListener(
			ComponentLayerEvent.LAYER_DEACTIVATE,
			modelLayerDeactivateHandler);
		
		layer.addEventListener(
			ModelLayerEvent.MODEL_ADD, 
			modelAddHandler);
		layer.addEventListener(
			ModelLayerEvent.MODEL_REMOVE, 
			modelRemoveHandler);
		
		layer.activate(IEventDispatcher(host));
	}
	
	/**
	 * @private
	 */
	public function deactivate(host:IModelLayerAware, layer:IModelLayer):void
	{
		layer.deactivate(IEventDispatcher(host));
		
		layer.removeEventListener(
			ComponentLayerEvent.LAYER_ACTIVATE,
			modelLayerActivateHandler);
		layer.removeEventListener(
			ComponentLayerEvent.LAYER_DEACTIVATE,
			modelLayerDeactivateHandler);			
		
		layer.removeEventListener(
			ModelLayerEvent.MODEL_ADD, 
			modelAddHandler);
		layer.removeEventListener(
			ModelLayerEvent.MODEL_REMOVE, 
			modelRemoveHandler);
	}
	
	/**
	 * @private
	 */
	public function activateCoreModel(
		host:ICoreModelAware, model:IComponentModelCore):void
	{
		// TODO (mschmalle) unit test
		var api:Class = ToolKitUtil.getClass(model);
		var component:IEventDispatcher = IEventDispatcher(host);
		var info:ModelInfo = registry.createInfo(component, model, api) as ModelInfo;
		
		activateModel(info);
	}
	
	/**
	 * @private
	 */
	public function deactivateCoreModel(
		host:ICoreModelAware, model:IComponentModelCore):void
	{
		// TODO (mschmalle) unit test
		var api:Class = ToolKitUtil.getClass(model);
		var component:IEventDispatcher = IEventDispatcher(host);
		var info:ModelInfo = registry.getInfo(component, api) as ModelInfo;
		
		deactivateModel(info);
	}
	
	//--------------------------------------------------------------------------
	//
	//  Private Class :: Handlers
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @private
	 * 1. add PROPERTY_CHANGE event to service for any [Bindable] variables
	 * 2. loop through each [ServiceBinding] of the component
	 * 3. add 'myProperty' + Change or PROPERTY_CHANGE listener
	 */
	private function activateModel(info:ModelInfo):void
	{
		var propertyChangeAdded:Boolean = false;
		var model:IComponentModel = info.model;
		var dispatcher:IEventDispatcher = info.component;
		
		// listen for the 'propertyChange' event on the model
		model.addEventListener(
			PropertyChangeEvent.PROPERTY_CHANGE, modelPropertyChanged);
		
		var data:ClassMetaData = ToolKitUtil.resolveClassMetaData(dispatcher);
		var bindings:Array = data.modelBindings;
		
		// even though there may be bindings, if the model didn't specify
		// value interests, kill the bindings list anyway
		if (!info.hasInterests())
			bindings = [];
		
		for each(var modelBinding:ModelBindingInfo in bindings)
		{
			// if this binding is not found in the value interests, skip
			if (!info.isInterest(modelBinding.id))
				continue;
			
			// if this is a propertyChange binding event, add it if not already
			if (modelBinding.binding == PropertyChangeEvent.PROPERTY_CHANGE)
			{
				if (!propertyChangeAdded)
				{
					// [Bindable]
					// listen for the 'propertyChange' event on the component					
					dispatcher.addEventListener(
						PropertyChangeEvent.PROPERTY_CHANGE,
						modelBindingPropertyChangeHandler);
					
					propertyChangeAdded = true;
				}
			}
			else
			{
				// [Bindable("modelPropertyChanged")]
				// listen for the 'modelPropertyChanged' event on the component
				dispatcher.addEventListener(
					modelBinding.binding,
					modelBindingHandler);			
			}
		}
		
		// update the component's API
		updateComponentOrModelAPI(info);
	}
	
	/**
	 * @private
	 */
	private function deactivateModel(info:ModelInfo):void
	{
		var model:IComponentModel = info.model;
		var dispatcher:IEventDispatcher = info.component;
		
		model.removeEventListener(
			PropertyChangeEvent.PROPERTY_CHANGE, modelPropertyChanged);		
		
		dispatcher.removeEventListener(
			PropertyChangeEvent.PROPERTY_CHANGE,
			modelBindingPropertyChangeHandler);	
		
		var data:ClassMetaData = ToolKitUtil.resolveClassMetaData(dispatcher);
		var bindings:Array = data.modelBindings;
		for each(var binding:ModelBindingInfo in bindings)
		{
			if (binding.binding != PropertyChangeEvent.PROPERTY_CHANGE)
			{
				dispatcher.removeEventListener(
					binding.binding,
					modelBindingHandler);
			}
		}
		
		registry.removeInfo(dispatcher, info.api);
	}	
	
	/**
	 * Handles when a service is added to a <code>IServiceLayerAware</code>
	 * component.
	 * 
	 * @param event A service event containing current service inforamtion.
	 */
	private function modelAddHandler(event:ModelLayerEvent):void
	{
		var layer:IModelLayer = event.currentTarget as IModelLayer;
		var info:ModelInfo = registry.createInfo(layer.component, event.model, event.api) as ModelInfo;
		
		activateModel(info);
	}
	
	/**
	 * Handles when a service is removed from a <code>IServiceLayerAware</code>
	 * component.
	 * 
	 * @param event A service event containing current service inforamtion.
	 */
	private function modelRemoveHandler(event:ModelLayerEvent):void
	{
		var layer:IModelLayer = event.currentTarget as IModelLayer;
		var info:ModelInfo = registry.getInfo(layer.component, event.api) as ModelInfo;
		
		deactivateModel(info);
	}
	
	/**
	 * @private
	 */
	private function modelLayerActivateHandler(event:ComponentLayerEvent):void
	{
	}
	
	/**
	 * @private
	 */
	private function modelLayerDeactivateHandler(event:ComponentLayerEvent):void
	{
	}
	
	//--------------------------------------------------------------------------
	//
	//  Private Class ServiceBinding :: Handlers
	//
	//--------------------------------------------------------------------------
	
	/**
	 * @private
	 * Handles component.PropertyChangeEvent.PROPERTY_CHANGE events that have 
	 * [ModelBinding] contracts.
	 */	
	private function modelBindingPropertyChangeHandler(event:PropertyChangeEvent):void
	{
		var property:String = event.property.toString();
		var source:IEventDispatcher = event.source as IEventDispatcher;
		if (!source)
			return;
		
		var value:* = event.newValue;
		setModelProperty(source, property, value);
	}
	
	/**
	 * @private
	 * Handles component.myProperty + "Changed" events that have 
	 * [ModelBinding] contracts.
	 */
	private function modelBindingHandler(event:Event):void
	{
		var source:IEventDispatcher = event.currentTarget as IEventDispatcher;
		if (!source)
			return;		
		
		var property:String = event.type.replace("Changed", "");
			
		try {

			if (property in source)
			{
				var value:* = source[property];
				setModelProperty(source, property, value);				
			}
			
		}
		catch (e:ReferenceError)
		{
			trace(e);
		}
	}


	/**
	 * Searches the service map using the component, string api and string
	 * property name.
	 * 
	 * If the string api is not directly found in some cases (coreModels
	 * that do not specify a direct api), 
	 */
	private function getModelInfo(
		component:IEventDispatcher, api:String, property:String):ModelInfo
	{
		// for empty [ModelBinding] meta which means bind to coreModel
		if (api == "" && component is ICoreModelAware)
		{
			var core:IComponentModelCore = ICoreModelAware(component).coreModel;
			if (core)
				api = ToolKitUtil.getClassName(core);
			else
				return null;
		}
		
		// get the Class definition of the api defined in the [ModelBinding]
		try {
			var apiClass:Class = ToolKitUtil.getDefinition(api) as Class;
		}
		catch (e:ReferenceError)
		{
			trace("MM.getModelInfo(" + property + ")", e);
			return null;
		}
		
		var info:ModelInfo = registry.getInfo(component, apiClass) as ModelInfo;
		// map of model instances
		var map:Object = registry.getInfoMap(component);
		
		if (!info || (info && !info.model))
		{
			// foreach string key in the Model map for the component
			for each (info in map)
			{
				// is this Model the same Class type?
				// does this Model contain the property name
				if (info.model is apiClass && property in info.model)
					break;
			}
		}
		
		return info;
	}

	/**
	 * @private
	 */
	private function setComponentProperty(
		source:IEventDispatcher, property:String, value:*):void
	{
		if (blockChangeEvents)
			return;
				
		if (source[property] == value)
			return;
		
		blockChangeEvents = true;
		
		source[property] = value;
		
		blockChangeEvents = false;
		
		// let all listeners of Component know property is updated
		updateProperty(source, property);
	}

	/**
	 * @private
	 */
	private function setModelProperty(
		source:IEventDispatcher, property:String, value:*):void
	{
		if (blockChangeEvents)
			return;
				
		var data:ClassMetaData = ToolKitUtil.resolveClassMetaData(source);
		var binding:ModelBindingInfo = data.getModelBindingByID(property);
		
		if (binding)
		{
			var info:ModelInfo = getModelInfo(source, binding.api, property);
			if (info == null)
			{
				// must have had a reference error, abort
				return;
			}
			
			try {
				if (property in info.model)
				{
					// we block change events here since actually assigning the
					// source property a value might come back to this helper and be henadled
					// in the modelPropertyChanged() handler. The update event is blocked
					// in that handler if this flag is true
					
					blockChangeEvents = true;
					
					if (info.model[property] != value)
						info.model[property] = value;
					
					blockChangeEvents = false;
				}
			}
			catch(e:ReferenceError)
			{
				trace(e);
			}
		}
	}
	
	//--------------------------------------------------------------------------
	//
	//  Private Class Update API :: Handlers
	//
	//--------------------------------------------------------------------------
		
	/**
	 * Handles when a IComponentModel property changes and sends a changed
	 * event through the component.
	 * 
	 * <p>The property must be [Bindable] or dispatch a 
	 * <code>PropertyChangeEvent.PROPERTY_CHANGE</code> event.</p>
	 */
	private function modelPropertyChanged(event:PropertyChangeEvent):void
	{
		if (blockChangeEvents)
			return;
		
		var model:IComponentModel = event.currentTarget as IComponentModel;
		var name:String = event.property.toString();
		
		var info:ModelInfo = registry.getInfoFromModel(model) as ModelInfo;
		
		// This syncronizes the Component to Model if a property is set 
		// from a Model and the Component is using the PropertyChange method
		
		if (!info || !info.hasInterests())
			return;
		
		if (info.isInterest(name) && name in model.component)
		{
			setComponentProperty(model.component, name, model[name])
		}
	}
	
	/**
	 * @private
	 * 
	 * Called from activateModel(). 
	 * 
	 * 1. set coreModel
	 * 2. uset coreModel
	 * 
	 * 3. modelLayer.registerModel()
	 * 4. modelLayer.removeModel()
	 * 
	 * This happens when coreModel = model is called.
	 * 
	 * This needs an external param like push or pull values into component from model.
	 * 
	 * - use component initial values (push to model) [pre addChild()]
	 * - use model initial values (push to component) [pre addChild()]
	 * 
	 * This happens when registerModel() is called AFTER addChild().
	 * 
	 * - use component current values (push to model) [post addChild()]
	 * - use model current values (push to component) [post addChild()]
	 */
	private function updateComponentOrModelAPI(info:ModelInfo):void
	{
		var interests:Array = info.model.listValueInterests();
		if (interests == null)
			return;
		
		updateValueInterestAPI(info);
	}
	
	/**
	 * @private (will only get called if listValueInterests() returns array)
	 */
	private function updateValueInterestAPI(info:ModelInfo):void
	{
		var component:IEventDispatcher = info.component;	
		var model:IComponentModel = info.model;
		
		// foreach listed interest in the model
		for each (var property:String in info.interests)
		{
			if (!(property in component))
			{
				continue; // TODO (bad listValueInterests() value
			}
			
			// addChild() phase
			if (!IToolKitComponent(component).toolKitInitialized)
			{
				if (model.dx_internal::initializeComponent)
				{
					// component is synced to model's initialized values
					setComponentProperty(component, property, model[property]);
				}
				else
				{
					// model is synced to component's initialized values
					setModelProperty(component, property, component[property]);
				}
			}
			// set coreModel, registerModel() phase
			else
			{
				// if the model has explicitly ordered an overwrite of component values;
				// component values are syncronized with model's current values
				if (model.dx_internal::resetComponent)
				{
					setComponentProperty(component, property, model[property]);
				}
				// model values are syncronized with component's current values
				else
				{
					setModelProperty(component, property, component[property]);
				}
			}
		}
		
		model.dx_internal::initializeComponent = false;
		model.dx_internal::resetComponent = false;
	}
	
	/**
	 * @private
	 * 
	 * Called when a component's property is set using setComponentProperty().
	 */
	private function updateProperty(component:IEventDispatcher, name:String):void
	{
		component.dispatchEvent(new Event(name + "Changed"));
	}
	
	//--------------------------------------------------------------------------
	//
	//  Public Class :: Methods
	//
	//--------------------------------------------------------------------------
	
    /**
     * @private
     */
    public static function getInstance():IModelManager
    {
        if (!instance) 
			instance = new ModelManagerImpl();
        return instance;
    }	
}
}